nohm

头像
_Sunshine
    阅读 13 分钟
    1

    Overview(概述)

    nohm是一个redis的orm(Object Relational Mapping)框架。这篇指引将让你较好的理解nohm是如何工作的。

    Basics(基础)

    在开始使用nohm之前有一些事情必须要做。如果你只是想确切的知道如何使用nohm的models,可以跳过这里直接到下一部分的Models。
    注意:这里几乎所有的代码例子都假定如下代码:

    var nohm = require('nohm').Nohm;
    var redisClient = require('redis').createClinet();
    nohm.setClient(redisClient);
    

    Prefix(前缀)

    第一件要做的事情是给redis keys设置一个前缀。这个前缀在你所使用的redis数据库中应该是唯一的,否则,将会遇到冲突。可以通过下面的代码来完成设置:

    nohm.setPrefix('yourAppPrefixForRedis');
    

    Client(客户端)

    你需要给nohm设置一个redis的客户端。在设置之前,需要连接到REDIS并选择一个合适的数据库。

    var redisClient = require('redis').createClient();
    redisClient.select(4);
    nohm.setClient(redisClient);
    

    Logging(日志)

    默认的nohm仅仅记录它遇到的错误到控制台。然而你可以重写logError方法做你任何想做的事。

    //this will throw all errors nohm encounters
    nohm.logError = function (err) {
        throw new Error({
            name: "Nohm Error",
            message :err
        });
    }
    

    Models(模型)

    你首先应该定义一个模型。一个模型需要一个名字和一些属性并且可以可选的有一些自定义的方法和一个REDIS客户端。

    var someModel = nohm.model('YourModelName',{
        properties:{
            //here you will define your properties
        },
        methods:{
            //here you will define your custom methods
        },
        client:someRedisClient // optional
    });
    

    严格的讲属性一样是可选的,但是没有属性的模型是没有意义的。
    第一个参数是模型的名字,将在nohm的内部使用用于redis keys和relations(将在后续章节介绍)。在整个应用程序中,它应该是唯一的(prefix/db)。第二个参数是一个对象包含了属性,方法和客户端。

    Methods(方法)

    如何你希望你的模型有一些自定义的方法,你可以在这定义他们。他们将绑定到模型并且因为在它们内部的'this'关键字就是模型的实例。

    Client(客户端)

    你可以可选的为一个模型设置一个redis客户端。这意味着从理论上你可以存储每一个模型到不同的redis数据库上。(我一点也不建议这样!)重要:当前,models之间的relations在不同的数据下不能工作!如果有人感兴趣的话,这一功能可能在接下来实现。

    Properties(属性)

    定义属性是模型最重要的部分。一个模型可以有以下一些选项:(将在后面进行详细解释)

    type(类型)字符串/函数

    该属性可变的类型/行为(behaviour)。所有的值将被转化为这个值。(这话是什么意思T_T)。
    有几种内建的类型:
    string,integer,float,boolean,timestamp和json
    你也可以定义一个行为。这是一个类型转化函数,将值转化为任何你想要的形式。
    

    defaultValue(默认值) 任何值

    当模型初始化时(感觉说实例化更好)一个属性拥有的默认值。
    甚至可以是一个函数,在每次一个新的实例被创建时被调用并且返回属性将使用的值。
    注意:如果你不定义一个默认值,默认值将会是0。
    

    validations(验证)字符串数组/多个数组/多个函数

    一个验证的数组。有一些内建的但是你也可以定义自定义的函数。
    

    index(索引)布尔值

    该值是否应该进行索引。这将会使查找更容易更简单,但是将会在redis数据库中额外多存储一些字节。
    

    unique(唯一性)布尔值

    在该模型的所有实例中该属性的值是否应该是唯一的。(大小写不敏感。空字符串不算作唯一。)
    

    load_pure(纯加载) 布尔值

    默认值为false并且加载的值都会进行类型转化(包括使用行为(Behaviour)进行转化)。如果你不想进行转化可以把该值设置为true,然后就不会更改redis传送过来的数据了。
    

    这是一个非常基本的的user的model:

    var User = nohm.model('User', {
      properties: {
        name: {
          type: 'string',
          unique: true,
          validations: [
            ['notEmpty']
          ]
        },
        email: {
          type: 'string',
          unique: true,
          validations: [
            ['notEmpty'],
            ['email']
          ]
        },
        password: {
          defaultValue: '',
          type: function (value) {
            return value + 'someSeed'; // and hash it of course, but to make this short that is omitted in this example
          },
          validations: [
            ['length', {
              min: 6
            }]
          ]
        },
        visits: {
          type: 'integer',
          index: true
        }
      }
    });
    

    Types/Behaviours(类型/行为)

    String

    普通的javascript string。
    

    Integer / Float

    值将被转化为Int(基是10)或者float,如果是NaN,则默认为0.
    

    Boolean

    转化为boolean-除了字符串'false'将被转化为布尔型的false
    

    Timestamp

    转化一个日期到一个时间戳(基为10的整数,距离1970年的毫秒数)。
    支持的格式:
    *Numbers 将使用parseInt进行转化
    *ISO时间字符串(带时区)
    *任何new Date()可以处理的时间字符串

    JSON

    如果输入的是一个合法的JSON字符串,则什么都不做。任何其他东西将使用JSON.stringify进行转化。注意JSON类型的属性将被作为已解析的对象返回。

    Behaviour(行为)

    可以是任何你想要的函数。它的this关键字是模型的实例,它接收三个参数:new_value,name和old_value,函数的返回值将作为属性的新值。注意,redis客户端在存储之前将转化所有的东西到字符串。
    一个例子:

    var User = nohm.model('User', {
      properties: {
        balance: {
          defaultValue: 0,
          type: function changeBalance(value, key, old) {
            return old + value;
          }
        }
      }
    });
    
    var test = new User();
    test.p('balance'); // 0
    test.p('balance', 5);
    test.p('balance'); // 5
    test.p('balance', 10);
    test.p('balance'); // 15
    test.p('balance', -6);
    test.p('balance'); // 9
    

    Validators(验证器)

    一个属性可以有多个验证器。当一个模型被保存他们将被调用或者手动调用他们。
    一个属性的校验被定义为一个字符串数组,对象和函数。
    函数必须是异步的并且3个函数:new_value,options和callback.callback接收一个参数:(bool)值是否合法。
    注意:这种函数不能导出到浏览器(浏览器不是异步的)
    三种方式的例子:

    var validatorModel = nohm.model('validatorModel', {
      properties: {
        builtIns: {
          type: 'string',
          validations: [
            'notEmpty',
            ['length', {
              max: 20 // 20 will be the second parameter given to the maxLength validator function (the first being the new value)
            }]
          ]
        },
        optionalEmail: {
          type: 'string',
          unique: true,
          validations: [
            ['email', {
              optional: true // every validation supports the optional option
            }]
          ]
        },
        customValidation: {
          type: 'integer',
          validations: [
            function checkIsFour(value, options, callback) {
              callback(value === 4);
            }
          ]
        }
      }
    });
    

    可以从文档里找到内建验证API或者是直接查看源码。

    在其他文件自定义验证

    如果你需要用函数自定义验证并且希望他们能够被到处到浏览器用于验证,则你需要从外部文件加载他们。
    例子 customValidation.js:

    exports.usernameIsAnton= function (value, options) {
      if (options.revert) {
        callback(value !== 'Anton');
      } else {
        callback(value === 'Anton');
      }
    };
    

    像下面这样加载:
    Nohm.setExtraValidations('customValidation.js')
    现在在模型定义里使用该验证像下面这样:

    nohm.model('validatorModel', {
      properties: {
        customValidation: {
          type: 'string',
          validations: [
            'usernameIsAnton',
            // or
            ['usernameIsAnton', {
              revert: true
            }]
          ]
        }
      }
    });
    

    ID生成

    默认的,一个实例的ID是唯一的字符串并且是在第一次调用save的时候生成的。
    你可以选择一个自增的id或者提供一个自定义的函数来生成id。

    var incremental = nohm.model('incrementalIdModel', {
      properties: {
        name: {
          type: 'string',
          validations: [
            'notEmpty'
          ]
        }
      },
      idGenerator: 'increment'
    });
    //ids of incremental will be 1, 2, 3 ...
    
    var prefix = 'bob';
    var counter = 200;
    var step = 50;
    var custom = Nohm.model('customIdModel', {
      properties: {
        name: {
          type: 'string',
          defaultValue: 'tom',
          validations: [
            'notEmpty'
          ]
        }
      },
      idGenerator: function (cb) {
        counter += step;
        cb(prefix+counter);
      }
    });
    // ids of custom will be bob250, bob300, bob350 ...
    

    创建一个实例

    有两种基本方法创建一个模型的实例。

    手动

    使用关键字new在Nohm.model的返回值上

    var UserModel = Nohm.model('UserModel', {});
    var user = new UserModel();
    

    这有一个缺点就是你必须跟踪你的模型,持有UserModel这个变量。

    工厂

    这个方法更简单,推荐使用这个方法

    Nohm.model('UserModel', {});
    var user = Nohm.factory('UserModel');
    

    你也可以传入一个id和一个callback作为第二个和第三个参数来快速的从数据库中加载数据。

    Nohm.model('UserModel', {});
    var user = Nohm.factory('UserModel', 123, function (err) {
      if (err) {
        // db error or id not found
      }
    });
    

    Setting/Getting属性

    函数p/prop/property(都一样)获取设置一个实例的属性。

    var user = new User();
    user.p('name', 'test');
    user.p('name'); // returns 'test'
    user.p({
      name: 'test2',
      email: 'someMail@example.com'
    });
    user.p('name'); // returns 'test2'
    user.p('email'); // returns 'someMail@example.com'
    

    还有几个其他方法用于处理属性:
    allProperties, propertyReset, propertyDiff

    Validating

    模型的实例在保存的时候会自动进行验证,但是你也可以手动进行验证。在下面的代码里我们假定模型的验证器已经被定义并从user实例化。

    调用valid()

    user.p({
      builtIns: 'teststringlongerthan20chars',
      optionalEmail: 'hurgs',
      customValidation: 3
    });
    user.valid(false, false, function (valid) {
      if ( ! valid) {
        user.errors; // { builtIns: ['length'], optionalEmail: ['email'], customValidation: ['custom'] }
      } else {
        // valid! YEHAA!
      }
    });
    

    这里有几个需要注意的地方:
    *user的errors对象包含了上一次验证时每一个属性的错误(这是一个将会修复的问题)
    *valid的第一个参数是一个可选的属性的名字。如果设置了,只有特定的属性会被验证

    浏览器验证

    暂时省略

    保存

    自动保存实例时决定是在redis中创建它还是更新它是基于检查user.id的结果。这意味着如果你没有手动设置id或者是从数据库中加载进来的,将会创建一个新的实例。自动保存将验证整个实体。如果没有经过验证,什么都不会保存。

    user.save(function (err) {
      if (err) {
        user.errors; // the errors in validation
      } else {
        // it's in the db :)
      }
    });
    

    保存时可以包含一个可选的对象,该对象包含一些选项,如下:

    user.save({
        // If true, no events from this save are published
      silent: false,
    
        // By default if user was linked to two objects before saving and the first linking fails, the second link will not be saved either.
        // Set this to true to try saving all relations, regardless of previous linking errors.
      continue_on_link_error: false,
    
        // Set to true to skip validation entirely.
        // *WARNING*: This can cause severe problems. Think hard before using this
        // It skips checking *and setting* unique indexes It is also NOT passed to linked objects that have to be saved.
      skip_validation_and_unique_indexes: false
    }, function (err) {
    });
    

    删除

    调用remove()将会完整的将实例从数据库中删除,包括关系(但是不包括关系到的实例-所以这不是串联删除)。只对设置过id的(手动设置的或者使用load加载的)实例有效。

    var user = nohm.factory('User');
    user.id = 123;
    user.remove({ // options object can be omitted
      silent: true, // whether remove event is published. defaults to false.
    }, function (err) {
      // user is gone.
    });
    

    加载

    操作一个实例的属性必须先通过id加载实例。

    user.load(1234, function (err, properties) {
      if (err) {
        // err may be a redis error or "not found" if the id was not found in the db.
      } else {
        console.log(properties);
        // you could use this.allProperties() instead, which also gives you the 'id' property
      }
    });
    

    查找

    查找一个实例的ID(比如想通过id加载它)Nohm提供了几种简单的搜索功能。函数始终都是.find(),但是该函数做什么就要看传给它的参数是什么。

    查找一个模型的所有id

    简单的调用find()只带一个回调函数将会取得所有的ID。

    SomeModel.find(function (err, ids) {
        // ids = array of ids
      });
    

    通过索引查找

    通过指定索引来为你查找,你必须指定一个对象作为第一个参数。有三种索引:unique,simple和numeric。Unique是最快的并且如果你查找一个属性是unique(唯一的)所有其他的条件都会被忽略。可以在一次find调用中混合使用三种查找方式。当所有的查找都处理完以后,查找到的IDs的交集会被返回。想要限制/过滤/排序得到的结果,你需要手动编辑返回的数组。

    通过simple索引查找

    设置index为true的所有属性(类型为'string','boolean','json'或者自定义行为)都会创建simple索引。
    例子:

    SomeModel.find({
        someString: 'hurg'
        someBoolean: false
      }, function (err, ids) {
        // ids = array of all instances that have (somestring === 'hurg' && someBoolean === false)
      });
    

    通过numeric索引查找

    设置index为true的所有属性(类型为'integer','float','timestamp')都会创建numberic索引。搜索时传入的对象还可以包含几个过滤选项:min,max,offset和limit。使用了redis的zrangebyscore命令并且会将过滤选项不做修改直接传递给该命令。

    {
      min: '-inf',
      max: '+inf',
      offset: '+inf', // only used if a limit is defined
      limit: undefined
    }
    

    当使用offset时想指定一个无限的limit使用limit:0。
    例子:

    SomeModel.find({
        someInteger: {
          min: 10,
          max: 40,
          offset: 15, // this in combination with the limit would work as a kind of pagination where only five results are returned, starting from result 15
          limit: 5
        },
        SomeTimestamp: {
          max: + new Date() // timestamp before now
        }
      }, function (err, ids) {
    
      });
    

    Important:limit只是指定给你正在搜索的索引。在上面的例子中,将会限制someInteger的搜索结果为5,但是搜索someTimestamp是没有限制的。因此得到的所有结果将会是所有搜索的交集,仅仅是最后得到和最小搜索的limit数一样的ids。
    如果你为多个搜索指定了limit,你也可能得到0个结果,尽管每个搜索得到了多个结果,因为他们可能没有交集。
    建议只给一个搜索指定limit或者在回调函数中手动的limit结果数组,而且这样更简单。
    可以使用一个准确的numeric值并使用simple索引搜索的语法进行搜索。

    闭区间

    Zrangebyscore 引用:>默认的,通过min和max指定的区间是闭区间。通过给score添加一个特殊的前缀(可以指定一个开区间。
    在nohm中,你可以指定一个endpoints选项达到同样的效果,默认的为'[]'创建的是一个闭区间。
    例子:

    SomeModel.find({
        someInteger: {
          min: 10,
          max: 20,
          endpoints: '(]'     // exclude models that have someInteger === 10, but include 20
          // endpoints: '('   short form for the same as above
          // endpoints: '[)'  would mean include 10, but exclude 20
          // endpoints: '()'  would excludes 10 and 20
        }
      }, function (err, ids) {
    
      });
    

    排序

    你可以使用内建的.sort()方法通过几种途径来排序你的模型。不过手动的进行一些复杂的排序可能是个更好的主意。

    排序DB中的所有数据

    SomeModel.sort({ // options object
      field: 'name' // field is mandatory
    }, function (err, ids) {
      // ids is an array of the first 100 ids of SomeModel instances in the db, sorted alphabetically ascending by name
    });
    
    SomeModel.sort({
      field: 'name',
      direction: 'DESC'
    }, function (err, ids) {
      // ids is an array of the first 100 ids of SomeModel instances in the db, sorted alphabetically descending by name
    });
    
    SomeModel.sort({
      field: 'name',
      direction: 'DESC',
      start: 50
    }, function (err, ids) {
      // ids is an array of 100 ids of SomeModel instances in the db, sorted alphabetically descending by name - starting at the 50th
    });
    
    SomeModel.sort({
      field: 'name',
      direction: 'DESC',
      start: 50,
      limit: 50
    }, function (err, ids) {
      // ids is an array of 50 ids of SomeModel instances in the db, sorted alphabetically descending by name - starting at the 50th
    });
    
    
    // this
    SomeModel.sort({
      field: 'last_edit',
      start: -10,
      limit: 10
    }, function (err, ids) {
      // ids is an array of the 10 last edited instances in the model (provided last_edit is filled properly on edit)
    });
    // would have the same result as:
    SomeModel.sort({
      field: 'last_edit',
      direction: 'DESC',
      start: 0,
      limit: 10
    }, function (err, ids) {
      // ids is an array of the 10 last edited instances in the model (provided last_edit is filled properly on edit)
    });
    

    通过指定IDS排序一个子集

    如果你有一个IDS的数组并且希望他们是有序的,你可以使用相同的方法相同的选项并把数组当作第二个参数。同find()结合起来非常有用。

    // assuming car model
    Car.find({
      manufacturer: 'ferrari',
    }, function (err, ferrari_ids) {
      Car.sort({
          field: 'build_year'
        },
        ferrari_ids, // array of found ferrari car ids
        function (err, sorted_ids) {
          // sorted_ids =  max. 100 oldest ferrari cars
        }
      );
    });
    

    注意:如果性能是很重要的,做这种组合find/sort,可能自己做多次查询REDIS DB是个好主意。

    关系(Relations)

    关系(或者说连接)是在实例间动态定义而不是为model定义。不同于传统的使用RDBMS的ORMs,需要预先定义的表或者列来维护他们的关系。在nohm中,让一个模型的一个实例拥有与其他模型的实例间的关系,而该模型的其他实例并没有与其他模型实例间的关系,并不需要那么做(预定义表或者列)。

    一个简单的例子:我们有UserModel的两个实例:User1,User2.有RoleModel的三个实例:AdminRole,AuthorRole,UserManagerRole。一个用户可以有0-3个角色。这创建了一个N:M的关系。在传统的数据中,你现在需要一张pivot table(联接表),然后以某种方式告诉你的ORM应该使用这个表来映射他们关系。在nohm中,这一步是不需要的,我们仅仅需要告诉每个UserModel的实例它们有什么关系就可以。
    积极的一面是在关系上有更大的灵活性,消极的一面是在管理他们的关系上变得更复杂了。

    在nohm中,所有的关系都有一个名字对。默认的,这个名字对是'default'和'defaultForeign'。一个已经初始化的实例的关系是连接到"default"的是"defaultForeign"。(在自定义的名字后面会附加上"Foreign")。这又同上面所说,带来了积极的一面和消极的一面。

    一些例子:

    User1.link(AdminRole);
    User1.link(AuthorRole);
    User2.link(UserManagerRole, 'createdBy');
    User2.link(UserManagerRole, 'temp');
    

    现在(在保存之后)存在这些关系:
    *User1(default)->AdminRole(defaultForeign)
    *User1(default)->AuthorRole(defaultForeign)
    *User2(createdBy)-> UserManagerRole(createdByForeign)
    *User2(temp)->UserManagerRole(tempForeign)
    提示:起名的时候小心并且不要滥用它。

    连接

    用法:instance.link(otherInstance,[options,][callback])
    与其他的实例创建一个关系(连接)。最基本的用法是只使用第一个参数:
    User1.link(AdminRole);
    当User1保存的时候会把关系写到数据中。(不是在保存Admin的时候)

    var User = nohm.factory('User');
    User.link(AdminRole);
    User.save(function (err, is_link_error, link_error_model_name) {
      if ( ! err) {
        // User1 and Admin are saved
      } else {
        // an error occured while saving.
      }
    });
    

    这里发生了几件事情:
    首先User1是经过验证的。如果User1是无效的,将会传递error到save的回调函数。如果User1是有效的,User1将会被存储。如果Admin有一个ID,关系将会被存储并且save的回调函数会被调用。另外Admin是经过验证的。如果Admin是无效的一个可选的错误回调将被调用,然后执行流程回到保存User上。(传给save回调函数的参数可以是'invalid',true,Admin.modelName)。如果Admin是有效的,Admin会被存储,关系也会被存储 然后调用save的回调函数。
    这个过程无限深。然而这个过程不是原子的,所以单独的保存元素然后再连接可能是个更好的主意!
    连接可以接收一个可选的对象或者连接名字作为第二个参数。如果是个字符串,将假定是连接名字。可选的对象有2个可用的选项:

    User1.link(ManagerRole, {
      name: 'hasRole', // otherwise defaults to "default"
      error: function (error_mesage, validation_errors, object) {
        // this is called if there was an error while saving the linked object (ManagerRole in this case)
        // error_message is the error ManagerRole.save() reported
        // validation_errors is ManagerRole.errors
        // object is ManagerRole
      }
    });
    

    分离

    用法:instance.unlink(otherInstance, [options,] [callback])

    发布/订阅

    其他

    未完待续。。。。。。


    _Sunshine
    58 声望2 粉丝

    « 上一篇
    编译twemproxy
    下一篇 »
    搭建redis集群